/*!
 * chartjs-plugin-deferred
 * http://chartjs.org/
 * Version: 0.2.0
 *
 * Copyright 2016 Simon Brunel
 * Released under the MIT license
 * https://github.com/chartjs/chartjs-plugin-deferred/blob/master/LICENSE.md
 */
/* global window: false */
'use strict';

(function () {

    var Chart = window.Chart;
    var helpers = Chart.helpers;
    var STUB_KEY = '_chartjs_deferred';
    var MODEL_KEY = '_deferred_model';

    /**
     * Plugin based on discussion from Chart.js issue #2745.
     * @see https://github.com/chartjs/Chart.js/issues/2745
     */
    Chart.Deferred = Chart.Deferred || {};
    Chart.Deferred.defaults = {
        enabled: true,
        xOffset: 0,
        yOffset: 0,
        delay: 0
    };

    // DOM implementation
    // @TODO move it in Chart.js: src/core/core.platform.js
    Chart.platform = helpers.extend(Chart.platform || {}, {
        defer: function (fn, delay, scope) {
            var callback = function () {
                fn.call(scope);
            };
            if (!delay) {
                helpers.requestAnimFrame.call(window, callback);
            } else {
                window.setTimeout(callback, delay);
            }
        }
    });

    function computeOffset(value, base) {
        var number = parseInt(value, 10);
        if (isNaN(number)) {
            return 0;
        } else if (typeof value === 'string' && value.indexOf('%') !== -1) {
            return number / 100 * base;
        }
        return number;
    }

    function chartInViewport(instance) {
        var model = instance[MODEL_KEY];
        var canvas = instance.chart.canvas;

        // http://stackoverflow.com/a/21696585
        if (canvas.offsetParent === null) {
            return false;
        }

        var rect = canvas.getBoundingClientRect();
        var dy = computeOffset(model.yOffset || 0, rect.height);
        var dx = computeOffset(model.xOffset || 0, rect.width);

        return rect.right - dx >= 0
            && rect.bottom - dy >= 0
            && rect.left + dx <= window.innerWidth
            && rect.top + dy <= window.innerHeight;
    }

    function buildDeferredModel(instance) {
        var defaults = Chart.Deferred.defaults;
        var options = instance.options.deferred;
        var getValue = helpers.getValueOrDefault;

        if (options === undefined) {
            options = {};
        } else if (typeof options === 'boolean') {
            // accepting { options: { deferred: true } }
            options = {enabled: options};
        }

        return {
            enabled: getValue(options.enabled, defaults.enabled),
            xOffset: getValue(options.xOffset, defaults.xOffset),
            yOffset: getValue(options.yOffset, defaults.yOffset),
            delay: getValue(options.delay, defaults.delay),
            appeared: false,
            delayed: false,
            loaded: false,
            elements: []
        };
    }

    function onScroll(event) {
        var node = event.target;
        var stub = node[STUB_KEY];
        if (stub.ticking) {
            return;
        }

        stub.ticking = true;
        Chart.platform.defer(function () {
            var instances = stub.instances.slice();
            var ilen = instances.length;
            var instance, i;

            for (i = 0; i < ilen; ++i) {
                instance = instances[i];
                if (chartInViewport(instance)) {
                    unwatch(instance); // eslint-disable-line
                    instance[MODEL_KEY].appeared = true;
                    instance.update();
                }
            }

            stub.ticking = false;
        });
    }

    function isScrollable(node) {
        var type = node.nodeType;
        if (type === Node.ELEMENT_NODE) {
            var overflowX = helpers.getStyle(node, 'overflow-x');
            var overflowY = helpers.getStyle(node, 'overflow-y');
            return overflowX === 'auto' || overflowX === 'scroll'
                || overflowY === 'auto' || overflowY === 'scroll';
        }

        return node.nodeType === Node.DOCUMENT_NODE;
    }

    function watch(instance) {
        var canvas = instance.chart.canvas;
        var parent = canvas.parentElement;
        var stub, instances;

        while (parent) {
            if (isScrollable(parent)) {
                stub = parent[STUB_KEY] || (parent[STUB_KEY] = {});
                instances = stub.instances || (stub.instances = []);
                if (instances.length === 0) {
                    parent.addEventListener('scroll', onScroll, {passive: true});
                }

                instances.push(instance);
                instance[MODEL_KEY].elements.push(parent);
            }

            parent = parent.parentElement || parent.ownerDocument;
        }
    }

    function unwatch(instance) {
        instance[MODEL_KEY].elements.forEach(function (element) {
            var instances = element[STUB_KEY].instances;
            instances.splice(instances.indexOf(instance), 1);
            if (!instances.length) {
                helpers.removeEvent(element, 'scroll', onScroll);
                delete element[STUB_KEY];
            }
        });

        instance[MODEL_KEY].elements = [];
    }

    Chart.plugins.register({
        beforeInit: function (instance) {
            var model = instance[MODEL_KEY] = buildDeferredModel(instance);
            if (model.enabled) {
                watch(instance);
            }
        },

        beforeDatasetsUpdate: function (instance) {
            var model = instance[MODEL_KEY];
            if (!model.enabled) {
                return true;
            }

            if (!model.loaded) {
                if (!model.appeared && !chartInViewport(instance)) {
                    // cancel the datasets update
                    return false;
                }

                model.appeared = true;
                model.loaded = true;
                unwatch(instance);

                if (model.delay > 0) {
                    model.delayed = true;
                    Chart.platform.defer(function () {
                        model.delayed = false;
                        instance.update();
                    }, model.delay);

                    return false;
                }
            }

            if (model.delayed) {
                // in case of delayed update, ensure to block external requests, such
                // as interacting with the legend label, or direct calls to update()
                return false;
            }
        }
    });

}());
